package ca.utoronto.utm.csc301.euchre.gamemanager;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.ArrayList;

import ca.utoronto.utm.csc301.euchre.common.Card;
import ca.utoronto.utm.csc301.euchre.common.Rank;
import ca.utoronto.utm.csc301.euchre.common.Suit;
import ca.utoronto.utm.csc301.euchre.player.Player;

/**
 * Concrete implementation of the GameManager interface.
 * 
 * @author Game Manager Team 2
 */
public class EuchreManager implements GameManager {
	public static final int NUM_PLAYERS = 4;
	public static final int CARDS_PER_HAND = 5;
	private static final boolean VERBOSE = true;
	
	private List<Player> players;
	private Team teamA, teamB;
	
	private Deck deck;
	private List<Card> kitty;
	private List<List<LinkedHashMap<Integer, Card>>> history;
	private List<LinkedHashMap<Integer, Card>> currentRound;
	private LinkedHashMap<Integer, Card> currentTrick;
	private Suit trump;
	
	private int dealerId;
	private int firstPlayerId;
	private int trumpChooserId;
	
	private int teamAGoingAloneId;
	private int teamBGoingAloneId;
	
	private List<List<Card>> playersHands;
	
	public EuchreManager() {
		players = new ArrayList<Player>();
		playersHands = new ArrayList<List<Card>>();
		kitty = new ArrayList<Card>();
		history = new ArrayList<List<LinkedHashMap<Integer, Card>>>();
	}
	
	/**
	 * Print a message if verbose mode has been specified.
	 * @param m the message to be printed
	 */
	private void printMessage(String m) {
		if (VERBOSE)
			System.out.println(m);
	}
	
	/**
	 * Return whether the two cards have the same suit, treating the left bower as a trump suit card.
	 * @param a the first card to compare
	 * @param b the second card to compare
	 * @return whether the two cards have the same suit, considering the left bower as a trump suit card
	 */
	private boolean isSameSuit(Card a, Card b) {
		Suit aSuit = a.getSuit();
		Suit bSuit = b.getSuit();

		if (a.getRank() == Rank.JACK && aSuit == getTrumpSuit().getSuitOfTheSameColor()) {
			printMessage("Left bower detected in trick 1.");
			aSuit = getTrumpSuit();
		}
		if (b.getRank() == Rank.JACK && bSuit == getTrumpSuit().getSuitOfTheSameColor()) {
			printMessage("Left bower detected in trick 2.");
			bSuit = getTrumpSuit();
		}
		
		return aSuit == bSuit;
	}
	
	/**
	 * Return whether or not the card cardPlayed is a valid card considering that player playerID is the one whom played it.
	 * @param cardPlayed the card that player playerID played
	 * @param playerID the ID of the player whom played the card cardPlayed
	 * @return whether or not the card cardPlayed is a valid card considering that player playerID is the one whom played it
	 */
	private boolean isValidCard(Card cardPlayed, int playerID) {	
		List<Card> hand = playersHands.get(playerID);
		
		if (hand == null) {
			//printMessage("VALIDATE: 1");
			return false;
		}
		
		// if the played who played this card doesn't have it in his hand or if this card has been played before, return false
		if (!containsCard(hand, cardPlayed)) {
			//printMessage("VALIDATE: 2");
			return false;
		}
		
		if (cardAlreadyPlayed(cardPlayed)) {
			//printMessage("VALIDATE: 2.5");
			return false;
		}
		
		// if this card is the lead card, return true
		if (getPlayedCards().size() == 0) {
			//printMessage("VALIDATE: 3");
			return true;
		}

		Card leadCard = getPlayedCards().get(firstPlayerId);
		if (leadCard == null)
			printMessage("Warning: Lead card is null.");
		
		//printMessage("First player of this trick is " + firstPlayerId);
		//printMessage("teamAGoingAloneId; " + teamAGoingAloneId);
		//printMessage("teamBGoingAloneId; " + teamBGoingAloneId);

		// if this card is of the same suit as the lead card, return true
		if (isSameSuit(leadCard, cardPlayed)) {
			//printMessage("VALIDATE: 4");
			return true;
		}
		
		// if this player's hand contains a card of the same suit as the lead card, return false, otherwise return true
		if (handHasSuit(hand, leadCard)) {
			//printMessage("VALIDATE: 5");
			return false;
		} else {
			//printMessage("VALIDATE: 6");
			return true;
		}
	}
	
	/**
	 * Return whether the list of cards cards contains the card card
	 * @param cards the list of cards to be tested for the containment of the card card
	 * @param card the card to be checked for in the list of cards cards
	 * @return whether the list of cards cards contains the card card
	 */
	private boolean containsCard(List<Card> cards, Card card){
		for (int i = 0; i < cards.size(); i++) {
			if (card.equals(cards.get(i)))
				return true;
		}
		return false;
	}
	
	/**
	 * Return whether the list of cards hand contains at least one card of the suit 
	 * @param hand the list of cards to be tested for the containment of the suit
	 * @param card the card with the suit to be checked for the list of cards hand
	 * @return whether the list of cards hand contains at least one card of the suit
	 */
	private boolean handHasSuit(List<Card> hand, Card card) {
		for (int i = 0; i < hand.size(); i++) {
			if (isSameSuit(hand.get(i), card))
				return true;
		}
		return false;
	}
	
	/**
	 * Checks if the card card has already been played by any of the players so far in this round.
	 * @param card the card that we want to check whether it has been played before or not
	 * @return whether the card card has already been played by any of the players so far in this round
	 */
	private boolean cardAlreadyPlayed(Card card) {
		List<LinkedHashMap<Integer, Card>> history = getHistory();
		
		if (history == null) {
			return false;
		}
		
		for (int i = 1; i < history.size(); i++) {
			for (int j = 0; j < history.get(i).size(); j++) {
				if (!partnerHasGoneAlone(i) && !partnerHasGoneAlone(j))
					if (history.get(i).get(j).equals(card))
						return true;
			}
		}
		return false;
	}
	
	/**
	 * Generate a random integer between 0 and limit inclusive.
	 * @param limit
	 * @return a random integer between 0 and limit inclusive
	 */
	public static int getRandom(int limit) {
		return (int) (Math.random() * (limit + 1));
	}

	/**
	 * Return the id of the player whom is the current dealer.
	 * @return the id of the player whom is the current dealer
	 */
	@Override
	public int getDealerId() {
		return dealerId;
	}

	/**
	 * Return all the cards that have been played this far in the current round.
	 * @return all the cards that have been played this far in the current round
	 */
	@Override
	public List<LinkedHashMap<Integer, Card>> getHistory() {
		return currentRound;
	}

	/**
	 * Return all the cards that have been played this far in the current trick
	 * @return all the cards that have been played this far in the current trick
	 */
	@Override
	public LinkedHashMap<Integer, Card> getPlayedCards() {
		return currentTrick;
	}

	/**
	 * Return the id of the team mate of the player player.
	 * @param player the player whom we want to get the team mate's ID of
	 * @return the id of the team mate of the player player
	 */
	@Override
	public int getTeamMateId(Player player) {
		int result = -1;
		
		for (int i = 0; i < players.size(); i++) {
			if (players.get(i) == player) {
				result = (i + 2) % 4;
			}
		}
		
		return result;
	}

	/**
	 * Return the score of the team to which the player player belongs.
	 * @param player the player we belonging to the team of which we want to get the score of
	 * @return the score of the team to which the player player belongs
	 */
	@Override
	public int getTeamScore(Player player) {
		int result = -1;
		
		for (int i = 0; i < players.size(); i++) {
			if (players.get(i) == player) {
				if (i % 2 == 0) {
					result = teamA.getScore();
				} else {
					result = teamB.getScore();
				}
			}
		}
		
		return result;
	}
	
	/**
	 * Returns the top card of the kitty.
	 * @return the top card of the kitty
	 */
	@Override
	public Card getTopCard() {
		return kitty.get(kitty.size()-1);
	}

	/**
	 * Returns the trump suit.
	 * @return the trump suit
	 */
	@Override
	public Suit getTrumpSuit() {
		return trump;
	}

	/**
	 * Returns all the cards that have been played so far in this game.
	 * @return all the cards that have been played so far in this game
	 */
	@Override
	public List<List<LinkedHashMap<Integer, Card>>> getGameHistory() {
		return history;
	}

	/**
	 * Set the players players to be the players whom are about to play the current game.
	 * First two players are on one team, the last two players are on another team.
	 */
	@Override
	public void setPlayers(Player... players) {
		this.players.add(players[0]);
		this.players.add(players[2]);
		this.players.add(players[1]);
		this.players.add(players[3]);
		
		teamA = new Team(players[0], players[1]);
		teamB = new Team(players[2], players[3]);
	}

	/**
	 * Return whether the specified player's partner going it alone currently.
	 * @param playerId the id the player whom we are interested in
	 * @return whether the specified player's partner is going it alone currently
	 */
	private boolean partnerHasGoneAlone(int playerId) {
		return (playerId % 2 == 0 && teamAGoingAloneId != -1 && teamAGoingAloneId != playerId) 
			|| (playerId % 2 == 1 && teamBGoingAloneId != -1 && teamBGoingAloneId != playerId);
	}
	
	/**
	 * Initiate the dealing of a hand.
	 */
	private void dealHands() {
		// Deal out, for each player, 5 cards from the deck.
		for (int c = 0; c < NUM_PLAYERS; c++) {
			List<Card> newDeal = new ArrayList<Card>();
			
			for (int i = 0; i < CARDS_PER_HAND; i++) {
				Card dealtCard = deck.popCard();
				newDeal.add(dealtCard); 
			}
			
			// Set the player's internal hand as well as our own recording of that player's hand.
			players.get(c).setHand(newDeal);
			playersHands.add(newDeal);
		}
		
		// Add the remaining cards to the kitty.
		kitty = new ArrayList<Card>();
		for (int i = 0; i < 4; i++) {
			kitty.add(deck.popCard());
		}
		
		firstPlayerId = (dealerId + 1) % 4;
	}
	
	/**
	 * Initiates the first round of bidding in order to choose trump.
	 * @throws IllegalPlayException
	 */
	private void firstBiddingRound() throws IllegalPlayException {
		// bidding round 1
		for (int i = 0; i < NUM_PLAYERS; i++) {
			int currentPlayerId = (firstPlayerId + i) % 4;
			Player currentPlayer = players.get(currentPlayerId);
			
			// ask the player to make trump
			trump = currentPlayer.makeTrump(true);
			
			// if the player chose to make trump
			if (trump != null) {
				
				// identify the player whom chose trump
				trumpChooserId = currentPlayerId;
				
				// if the dealer's partner makes trump, they must go it alone 
				if (trumpChooserId == (dealerId + 2) % 4 || currentPlayer.isGoingAlone()) {
					if (trumpChooserId % 2 == 0) {
						if (teamAGoingAloneId == -1) {
							teamAGoingAloneId = trumpChooserId;
							
							int currentId = (trumpChooserId + 1) % 4;
							if (players.get(currentId).isGoingAlone()) {
								teamBGoingAloneId = currentId;
							} else if (players.get((currentId + 3) % 4).isGoingAlone()) {
								teamBGoingAloneId = currentId;
							}
						} else {
							throw new IllegalPlayException("There is already a player on team A going it alone!");
						}
					} else {
						if (teamBGoingAloneId == -1) {
							teamBGoingAloneId = trumpChooserId;
							
							int currentId = (trumpChooserId + 1) % 4;
							if (players.get(currentId).isGoingAlone()) {
								teamAGoingAloneId = currentId;
							} else if (players.get((currentId + 3) % 4).isGoingAlone()) {
								teamAGoingAloneId = currentId;
							}
						} else {
							throw new IllegalPlayException("There is already a player on team B going it alone!");
						}
					}
				}
				
				// this is the card to be dropped from the player's hand and added to the kitty
				Card c = currentPlayer.pick();
				
				// make sure they are dropping a card which they have
				if (!containsCard(playersHands.get(currentPlayerId), c) && !(c.getRank() == getTopCard().getRank() && c.getSuit() == getTopCard().getSuit())) {
					throw new IllegalPlayException("Player " + currentPlayerId + " tried to drop a card which they didn't have.");
				}
				
				// add the top card from the kitty to the player's hand
				playersHands.get(currentPlayerId).add(getTopCard());
				
				// remove the card the player chose to drop from their hand
				playersHands.get(currentPlayerId).remove(c);
				
				// remove the top card from the kitty
				kitty.remove(getTopCard());
				
				// add to the kitty the card that the player just dropped
				kitty.add(c);
				
				// don't ask the rest of the players to make trump
				break;
			}
		}		
	}
	
	/**
	 * Initiates the second round of bidding in order to choose trump.
	 * @throws IllegalPlayException
	 */
	private void secondBiddingRound() throws IllegalPlayException {
		// the suit of the top card in the kitty
		Suit kittySuit = getTopCard().getSuit();
		
		// bidding round 2
		for (int i = 0; i < NUM_PLAYERS; i++) {
			int currentPlayerId = (firstPlayerId + i) % 4;
			Player currentPlayer = players.get(currentPlayerId);
			
			trump = currentPlayer.makeTrump(false);
			
			if (trump == kittySuit)
				throw new IllegalPlayException("Player " + currentPlayerId + " tried to chose the kitty's top card's suit as trump in the second round of bidding\n");
			
			if (trump != null) {
				trumpChooserId = currentPlayerId;
				break;
			}
		}
	}
	
	/**
	 * Initiates the bidding in order to choose trump
	 * @throws IllegalPlayException
	 */
	private void bidForTrump() throws IllegalPlayException {
		firstBiddingRound();
		
		// if trump is still not chosen, start the second round of bidding
		if (trump == null) {
			secondBiddingRound();
		}
		
		printMessage("The trump is " + trump);
	}
	
	/**
	 * Initiate the playing of the trick.
	 * @throws IllegalPlayException
	 */
	private void playTrick() throws IllegalPlayException {
		// Allow each player to play in the current trick.
		for (int h = 0; h < NUM_PLAYERS; h++) {
			int currentPlayerId;
			
			// Let the players play in order starting from the first player, clockwise.
			currentPlayerId = (firstPlayerId + h) % 4;
			
			// Skip this player if they are going it alone
			if (partnerHasGoneAlone(currentPlayerId)) {
				printMessage("Player " + currentPlayerId + "'s partner is going it alone.\n");
			} else {
				Player currentPlayer = players.get(currentPlayerId);
				
				// Ensure that the player does not have more cards than they were dealt.
				if (playersHands.get(h).size() > 5) {
					throw new IllegalPlayException("Player " + h + " has an invalid hand size: " + playersHands.get(h).size());
				}
				
				// Get the current player's choice, validate, and throw an exception if the choice is illegal.
				Card currentPlay = currentPlayer.play();
				
				if (currentPlay == null) {
					throw new IllegalPlayException("Player " + currentPlayerId + " tried to play a null card.");
				}
				
				if (!isValidCard(currentPlay, currentPlayerId)) {
					System.err.println("Tried to play " + currentPlay.getRank() + " " + currentPlay.getSuit());
					throw new IllegalPlayException("Player " + currentPlayerId + " tried to make an invalid play!");
				}
				
				// Print some status information.
				printMessage("Player " + currentPlayerId + ":");
				String hand = "";
				for (Card c : playersHands.get(currentPlayerId))
					hand += "[" + c.getRank() + " " + c.getSuit() + "] ";
				printMessage("Hand: " + hand);
				printMessage("Plays: " + currentPlay.getRank() + " " + currentPlay.getSuit());
				
				// Reflect the player's choice on the game state.
				playersHands.get(currentPlayerId).remove(currentPlay);
				currentTrick.put(currentPlayerId, currentPlay);
			}
		}		
	}
	
	/**
	 * Initiate the playing of the round.
	 * @throws IllegalPlayException
	 */
	private void playRound() throws IllegalPlayException {
		int i = 0;
		int winnerId = -1;
		
		// Make sure we update the first player if this player's partner is going alone.
		for (int c = 0; c < 2; c++) {
			if (partnerHasGoneAlone(firstPlayerId)) {
				firstPlayerId = (firstPlayerId + 1) % 4;
			}
		}
	
		// For each card in a hand, play a trick.
		while (i < CARDS_PER_HAND) {
			currentTrick = new LinkedHashMap<Integer, Card>();
			
			// The last winner of this hand will go first, if there has been a winner.
			if (winnerId != -1)
				firstPlayerId = winnerId;
			
			playTrick();
			
			currentRound.add(currentTrick);
			
			// Give the appropriate team the victory based on the highest card played in the trick.
			winnerId = getWinner();
			printMessage("Player " + winnerId + " wins!");
			if (winnerId % 2 == 0) {
				teamA.addVictory();
			} else {
				teamB.addVictory();
			}
			
			printMessage("> Trick finished.\n");
			i++;
		}
		
		// Update the history of tricks and the scores for each team.
		printMessage(">>> Hand finished.\n");
		history.add(currentRound);
		updateScores();		
	}
	
	/**
	 * Main game loop.
	 * 
	 * @throws IllegalPlayException
	 */
	@Override
	public void startGame() throws IllegalPlayException {
		dealerId = getRandom(3);
		
		while (teamA.getScore() < 10 && teamB.getScore() < 10) {
			currentRound = new ArrayList<LinkedHashMap<Integer, Card>>();
			
			teamA.resetTricksWon();
			teamB.resetTricksWon();
			
			// Reset which players on each team are going it alone.
			teamAGoingAloneId = -1;
			teamBGoingAloneId = -1;
			
			// Reset the deck with all valid Euchre cards.
			deck = new Deck();
			
			// Shift the dealer "clockwise".
			dealerId = (dealerId + 1) % NUM_PLAYERS;
			
			// Clear the hands we have stored for players.
			playersHands.clear();
			
			dealHands();
			
			bidForTrump();
			
			// if trump is still not chosen after the second round of bidding, re-deal next hand
			if (trump == null) {
				continue;
			}
			
			playRound();
		}
		
		// Game over! Print the winner and the scores.
		if (teamA.getScore() > teamB.getScore()) {
			printMessage("Team A wins!");
		} else {
			printMessage("Team B wins!");
		}
		printMessage("Team A Scored " + teamA.getScore() + " points.");
		printMessage("Team B Scored " + teamB.getScore() + " points.");
	}
	
	/**
	 * Compare the ratings of the cards played and return the winner.
	 * @return The id of the winner of this trick.
	 */
	private int getWinner() {
		int winner = 0;
		
		// ensure that we do not start from a player who is not part of this trick
		for (int i = 0; i < 2; i++) {
			if (partnerHasGoneAlone(winner)) {
				winner++;
			}	
		}

		// check all other players who are a part of this hand for the strongest card played
		for (int i = 1; i < NUM_PLAYERS; i++) {
			int currentId = (winner + i) % 4;
			if (!partnerHasGoneAlone(currentId))
				if (getRating(getPlayedCards().get(currentId)) > getRating(getPlayedCards().get(winner)))
					winner = currentId;
		}
		
		return winner;
	}
	
	/**
	 * Return a numerical rating for the card card.
	 * @param card the card for which we need a rating for
	 * @return a numerical rating for the card card
	 */
	private int getRating (Card card) {	
		Suit leadSuit = getPlayedCards().get(firstPlayerId).getSuit();
		int rating = 1;
		Suit suit = card.getSuit();
		Rank rank = card.getRank();
		
		if (rank == Rank.JACK && suit == trump)
			rating = 14;
		else if (rank == Rank.JACK && suit.isOfTheSameColor(trump))
			rating = 13;
		else if (rank == Rank.ACE && suit == trump)
			rating = 12;
		else if (rank == Rank.KING && suit == trump)
			rating = 11;
		else if (rank == Rank.QUEEN && suit == trump)
			rating = 10;
		else if (rank == Rank.TEN && suit == trump)
			rating = 9;
		else if (rank == Rank.NINE && suit == trump)
			rating = 8;
		else if (suit == leadSuit) {
			if (rank == Rank.ACE)
				rating = 7;
			else if (rank == Rank.KING)
				rating = 6;
			else if (rank == Rank.QUEEN)
				rating = 5;
			else if (rank == Rank.JACK)
				rating = 4;
			else if (rank == Rank.TEN)
				rating = 3;
			else if (rank == Rank.NINE)
				rating = 2;
		}
			
		return rating;
	}
	
	/**
	 * Update both teams' scores after a round has been played.
	 */
	private void updateScores() {
		if (trumpChooserId % 2 == 0) {
			//Team A
			//Bidding partnership (makers) wins 3 or 4 tricks
			if (teamA.getTricksWon() == 3 || teamA.getTricksWon() == 4) {
				teamA.setScore(teamA.getScore() + 1);
			} else if (teamA.getTricksWon()== 5) {
				//Bidder goes alone and wins 5 tricks
				if (teamAGoingAloneId != -1 && teamA.getTricksWon() == 5) {
					teamA.setScore(teamA.getScore() + 4);
				} 
				//Bidder goes alone and wins 3 or 4 tricks
				else if (teamAGoingAloneId != -1 && (teamA.getTricksWon() == 4 || teamA.getTricksWon() == 3)) {
					teamA.setScore(teamA.getScore() + 1);
				}
				//Bidding partnership (makers) wins 5 tricks
				else{
					teamA.setScore(teamA.getScore() + 2);
				}
			} else {
				//Defenders win 3 or 4 tricks
				if(teamB.getTricksWon()== 3 || teamB.getTricksWon() == 4) {
					teamB.setScore(teamB.getScore() + 2);
				}
				//Defenders win 5 tricks
				else if (teamB.getTricksWon() == 5) {
					teamB.setScore(teamB.getScore() + 4);
				}else if (teamBGoingAloneId != -1  && teamB.getTricksWon() > 2) {
					//Lone defender (vs. lone bidder) wins 3 or more tricks
					if (teamAGoingAloneId != -1 == true) {
						teamB.setScore(teamB.getScore() + 8);
					}
					//Lone defender wins 3 or more tricks
					else{
						teamB.setScore(teamB.getScore() + 6);
					}
				}
			}
		} else {
			//Bidding partnership (makers) wins 3 or 4 tricks
			if (teamB.getTricksWon()== 3 || teamB.getTricksWon() == 4) {
				teamB.setScore(teamB.getScore() + 1);
			} else if (teamB.getTricksWon()== 5) {
				//Bidder goes alone and wins 5 tricks
				if (teamBGoingAloneId != -1 && teamB.getTricksWon() == 5) {
					teamB.setScore(teamB.getScore() + 4);
				} 
				//Bidder goes alone and wins 3 or 4 tricks
				else if (teamBGoingAloneId != -1 && (teamB.getTricksWon() == 4 || teamB.getTricksWon() == 3)) {
					teamB.setScore(teamB.getScore() + 1);
				}
				//Bidding partnership (makers) wins 5 tricks
				else {
					teamB.setScore(teamB.getScore() + 2);
				}
			} else {
				//Defenders win 3 or 4 tricks
				if (teamA.getTricksWon()== 3 || teamA.getTricksWon() == 4){
					teamA.setScore(teamA.getScore() + 2);
				}
				//Defenders win 5 tricks
				else if (teamA.getTricksWon() == 5) {
					teamA.setScore(teamA.getScore() + 4);
				} else if (teamAGoingAloneId != -1  && teamA.getTricksWon() > 2) {
					//Lone defender (vs. lone bidder) wins 3 or more tricks
					if (teamBGoingAloneId != -1 == true) {
						teamA.setScore(teamA.getScore() + 8);
					}
					//Lone defender wins 3 or more tricks
					else {
						teamA.setScore(teamA.getScore() + 6);
					}
				}
			}
		}
	}

	/**
	 * Returns the players id in order of the current hand. [0] is the player on the left of the dealer, ...
	 * @return the players id in order of the current hand. [0] is the player on the left of the dealer, ... 
	 */
	@Override
	public int[] getIdsInOrder() {
		int[] result = new int[NUM_PLAYERS];
		
		for (int i = 0; i < result.length; i++) {
			result[i] = (firstPlayerId + i) % 4;
		}
		
		return result;
	}
}
